#include <thread>
#include <mutex>
#include <atomic>
#include <chrono>
#include <random>
#include <string>
#include <iostream>

//-----------------------------------------------------------------------------

using uint = unsigned int;

class random_uints {
public:
    random_uints()
        : mt_engine_(std::random_device()())
    {}

    uint next(const uint upper_bound) {
        return uniform_uints_(mt_engine_) % upper_bound;
    }

private:
    std::mt19937 mt_engine_;
    std::uniform_int_distribution<uint> uniform_uints_;
};

//-----------------------------------------------------------------------------

void this_thread_sleep_msecs(const uint ms) {
    std::this_thread::sleep_for( std::chrono::milliseconds(ms) );
}

//-----------------------------------------------------------------------------

void log(const std::string& msg) {
    static std::mutex cout_mutex;
    std::lock_guard<std::mutex> lock(cout_mutex);

    std::cout << msg << std::endl;
}

//-----------------------------------------------------------------------------

class dining_table {
public:
    dining_table(const size_t num_seats)
        : num_seats_(num_seats)
        , forks_(num_seats)
    {}

    std::mutex& left_fork(const size_t seat) {
        return forks_[ left_fork_index(seat) ];
    }

    std::mutex& right_fork(const size_t seat) {
        return forks_[ right_fork_index(seat) ];
    }

    size_t left_fork_index(const size_t seat) const {
        return seat;
    }

    size_t right_fork_index(const size_t seat) const  {
        return (seat + 1) % num_seats_;
    }

    // non-copyable
    dining_table(const dining_table& that) = delete;
    dining_table& operator =(const dining_table& that) = delete;

    // non-movable
    dining_table(dining_table&& tmp) = delete;
    dining_table& operator =(dining_table&& tmp) = delete;

private:
    size_t num_seats_;
    std::vector<std::mutex> forks_;
};

//-----------------------------------------------------------------------------

class philosopher {
public:
    philosopher(dining_table& table, const size_t seat, std::atomic_bool& stop_requested)
        : table_(table)
        , seat_(seat)
        , stop_requested_(stop_requested)
        , eat_count_(0)
    {}

public:
    void operator()() {
        log("start");
        while (!stop_requested_) {
            think();
            take_forks_and_eat();
        }
        log(std::string("eat count = ") + std::to_string(eat_count_));
    }

private:
    void think() {
        log("thinking");
        this_thread_sleep_msecs( rand_uints_.next(10) );
    }

    void eat() {
        log("eating");
        ++eat_count_;
        this_thread_sleep_msecs( rand_uints_.next(10) );
    }

    void take_forks_and_eat() {
        log("hungry");

        std::mutex& left_fork = table_.left_fork(seat_);
        std::mutex& right_fork = table_.right_fork(seat_);

        /*
        {
            // #1

            std::lock_guard<std::mutex> left_fork_lock(left_fork);
            log("pick up left fork");

            this_thread_sleep_msecs( rand_uints_.next(10) );

            std::lock_guard<std::mutex> right_fork_lock(right_fork);
            log("pick up right fork");

            eat();

            log("put down forks");
        }
        */

        /*
        {
            // #2

            std::unique_lock<std::mutex> left_fork_lock(left_fork, std::defer_lock);
            std::unique_lock<std::mutex> right_fork_lock(right_fork, std::defer_lock);

            while (true) {
                left_fork_lock.lock();
                log("pick up left fork");
                if (right_fork_lock.try_lock()) {
                    log("pick up right fork");
                    eat();
                    break;
                } else {
                    left_fork_lock.unlock();
                    log("put down left fork");
                    std::this_thread::yield();
                }
            }

            log("put down forks");
        }
        */

        /*
        {
            // #3

            std::lock(left_fork, right_fork);
            log("pick up forks");
            std::unique_lock<std::mutex> left_fork_lock(left_fork, std::adopt_lock);
            std::unique_lock<std::mutex> right_fork_lock(right_fork, std::adopt_lock);
            eat();
            log("put down forks");
        }
        */

        /*
        {
            // #4

            if (seat_ == 0) {
                // left-handed
                std::lock_guard<std::mutex> left_fork_lock(left_fork);
                log("take left fork");
                std::lock_guard<std::mutex> right_fork_lock(right_fork);
                log("take right fork");
                eat();
                log("put down forks");
            } else {
                // right-handed
                std::lock_guard<std::mutex> right_fork_lock(right_fork);
                log("take right fork");
                std::lock_guard<std::mutex> left_fork_lock(left_fork);
                log("take left fork");
                eat();
                log("put down forks");
            }
        }
        */
    }

    void log(const std::string& action) {
        ::log(std::string("P") + std::to_string(seat_) + ": " + action);
    }

private:
    dining_table& table_;
    size_t seat_;

    std::atomic_bool& stop_requested_;

    size_t eat_count_;

    random_uints rand_uints_;
};

//-----------------------------------------------------------------------------

void dinner(const size_t num_participants) {
    dining_table table(num_participants);

    std::atomic_bool stop_requested(false);

    std::vector<std::thread> threads;
    for (size_t seat = 0; seat < num_participants; ++seat) {
        threads.emplace_back(
            philosopher(table, seat, stop_requested)
        );
    }

    // it's dinner time!
    std::this_thread::sleep_for(std::chrono::seconds(5));

    stop_requested.store(true);
    for (auto& t: threads) {
        t.join();
    }
}

//-----------------------------------------------------------------------------

int main() {
    dinner(5);
    return 0;
}
